"use client"; /** * /w/:workspace/settings/developers — Developers hub (TASK-0128 + 0130). * * Tabs (TASK-0130): Overview · API keys · Webhooks · Events · Curl examples. * * - Overview: base URL, copyable quickstart (pages + API-key + OAuth2 + * webhook-signature verification), links to /docs/api + the OpenAPI UI. * - API keys (GET/POST /workspaces/:id/api-keys, …/api-keys/:id): the raw * key (`fk_live_…`) is revealed EXACTLY ONCE (ApiKeyRevealDialog); the * list never includes it; scopes READ / WRITE / ADMIN (implied). * - Webhooks: list/create/rotate/pause/delete + the per-webhook * DeliveryInspector (log + payload/signature drawer + test send + * resubmit dead). Create/rotate reveal the secret ONCE. * - Events: the shared WEBHOOK_EVENT_CATALOG (single source of truth — * description, rendered payload schema, copyable example per event). * - Curl examples: per-resource snippets from docs/api/*.md (key placeholder). * * PERMISSIONS (server is authoritative — AGENTS rule 19): the server * enforces OWNER/ADMIN (403 `requires_workspace_role` otherwise) and * rejects API-key callers (`requires_user_credentials`). The UI hides the * tab for non-owners/admins as UX only. */ import { useState } from "react"; import { useParams } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { toast } from "@/stores/ui-store"; import { apiFetch } from "@/lib/api"; import { API_BASE } from "@/lib/constants"; import { useWorkspace } from "@/hooks/use-workspace"; import { SettingsShell, Section } from "@/components/settings/settings-shell"; import { ApiKeyCreateDialog } from "@/components/settings/api-keys/ApiKeyCreateDialog"; import { ApiKeyRevealDialog } from "@/components/settings/api-keys/ApiKeyRevealDialog"; import { ApiKeyTable, type ApiKeyRow } from "@/components/settings/api-keys/ApiKeyTable"; import { OverviewTab } from "@/components/settings/webhooks/OverviewTab"; import { WebhooksTab } from "@/components/settings/webhooks/WebhooksTab"; import { EventExplorer } from "@/components/settings/webhooks/EventExplorer"; import { CurlExamples } from "@/components/settings/webhooks/CurlExamples"; import { cn } from "@/lib/utils"; type Tab = "overview" | "keys" | "webhooks" | "events" | "curl"; /** Accepts a bare array or a { data: […] } envelope (API conventions). */ function unwrap(body: unknown): T[] { const b = body as { data?: T[] } | T[] | null; if (Array.isArray(b)) return b; if (b && typeof b === "object" && Array.isArray((b as { data?: T[] }).data)) return (b as { data: T[] }).data; return []; } export default function SettingsDevelopersPage() { const params = useParams<{ workspace: string }>(); const slugOrId = params?.workspace ?? ""; const { data: ws, isLoading } = useWorkspace(slugOrId); const wid = ws?.id ?? ""; const [tab, setTab] = useState("keys"); // The API returns UPPERCASE roles (the Prisma enum); the web type is // lowercase — normalize like the other settings pages. const role = (ws?.role ?? "").toLowerCase(); const isAdminRole = role === "owner" || role === "admin"; return ( {isLoading || !ws ? (
{[0, 1, 2].map((i) => (
))}
) : !isAdminRole ? ( // UX-only guard (rule 19 — the server enforces 403); a direct URL // still resolves here, so say plainly who may act.

Ask a workspace owner to manage API keys for you, or request the Admin role.

) : (
{( [ { id: "overview", label: "Overview" }, { id: "keys", label: "API keys" }, { id: "webhooks", label: "Webhooks" }, { id: "events", label: "Events" }, { id: "curl", label: "Curl examples" }, ] as { id: Tab; label: string }[] ).map((t) => ( ))}
{tab === "overview" ? : null} {tab === "keys" ? : null} {tab === "webhooks" ? : null} {tab === "events" ? (
) : null} {tab === "curl" ? : null}
)} ); } /* ------------------------------------------------------------- API keys */ function ApiKeys({ workspaceId }: { workspaceId: string }) { const qc = useQueryClient(); const [createOpen, setCreateOpen] = useState(false); const [reveal, setReveal] = useState<{ rawKey: string; name: string } | null>(null); const [revokingId, setRevokingId] = useState(null); const keys = useQuery({ queryKey: ["workspaces", workspaceId, "api-keys"], queryFn: async (): Promise => unwrap(await apiFetch(`/workspaces/${workspaceId}/api-keys`)), enabled: !!workspaceId, }); const create = useMutation({ mutationFn: async (input: { name: string; scopes: string[]; rateLimit?: number; expiresAt?: string; }) => { const res = await apiFetch<{ key: string }>(`/workspaces/${workspaceId}/api-keys`, { method: "POST", body: input, }); return res; }, onSuccess: () => qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "api-keys"] }), }); const revoke = useMutation({ mutationFn: (id: string) => apiFetch(`/workspaces/${workspaceId}/api-keys/${id}`, { method: "DELETE" }), onMutate: (id) => setRevokingId(id), onSettled: () => setRevokingId(null), onSuccess: () => qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "api-keys"] }), onError: (e) => toast("error", (e as Error).message || "Could not revoke the key."), }); // The API origin (already includes /api/v1 — the web has no Next /api // proxy, it calls the API directly). const baseApiUrl = API_BASE; return (
revoke.mutate(row.id)} revokingId={revokingId} onCreate={() => setCreateOpen(true)} /> setCreateOpen(false)} onCreated={(k) => { setCreateOpen(false); setReveal(k); }} create={create.mutateAsync} isPending={create.isPending} /> {reveal ? ( setReveal(null)} /> ) : null}
); }